import fs from 'fs';
import path from 'path';

export default (source, dest, opts, callback) => {
	let options = opts || {},
		basePath = process.cwd(),
		currentPath = path.resolve(basePath, source),
		targetPath = path.resolve(basePath, dest),
		ignore = options.ignore || null,
		started = 0,
		finished = 0,
		running = 0,
		limit = 32;

	const startCopy = (filepath) => {
		started++;
		if (ignore) {
			if (!ignore.test(filepath)) {
				if (options.log) console.log('Cloned', filepath.replace(basePath, ''));
				return getStats(filepath);
			} else {
				return cb(true);
			}
		} else {
			if (options.log) console.log('Cloned', filepath.replace(basePath, ''));
			return getStats(filepath);
		}
	};

	const getStats = (source) => {
		const stat = fs.lstat;
		if (running >= limit) {
			return setImmediate(() => {
				getStats(source);
			});
		}
		running++;
		stat(source, (err, stats) => {
			const item = {};
			if (err) {
				return console.log(err);
			}

			// We need to get the mode from the stats object and preserve it.
			item.name = source;
			item.mode = stats.mode;
			item.mtime = stats.mtime; //modified time
			item.atime = stats.atime; //access time

			if (stats.isDirectory()) {
				return onDir(item);
			} else if (stats.isFile()) {
				return onFile(item);
			} else if (stats.isSymbolicLink()) {
				// Symlinks don't really need to know about the mode.
				return onLink(source);
			}
		});
	};

	const onFile = (file) => {
		const target = file.name.replace(currentPath, targetPath);

		isWritable(target, (writable) => {
			if (writable) {
				return copyFile(file, target);
			} else {
				return cb();
			}
		});
	};

	const copyFile = (file, target) => {
		const readStream = fs.createReadStream(file.name),
			writeStream = fs.createWriteStream(target, {
				mode: file.mode
			});
		writeStream.on('open', () => {
			readStream.pipe(writeStream);
		});
		writeStream.once('finish', () => {
			cb();
		});
	};

	const rmFile = (file, done) => {
		fs.unlink(file, (err) => {
			if (err) {
				return console.log(err);
			}
			return done();
		});
	};

	const onDir = (dir) => {
		const target = dir.name.replace(currentPath, targetPath);
		isWritable(target, (writable) => {
			if (writable) {
				return mkDir(dir, target);
			}
			copyDir(dir.name);
		});
	};

	const mkDir = (dir, target) => {
		fs.mkdir(target, dir.mode, (err) => {
			if (err) {
				return console.log(err);
			}
			copyDir(dir.name);
		});
	};

	const copyDir = (dir) => {
		fs.readdir(dir, (err, items) => {
			if (err) {
				return console.log(err);
			}
			for (let i = 0; i < items.length; i++) {
				const item = items[i];
				startCopy(path.join(dir, item));
			}
			return cb();
		});
	};

	const onLink = (link) => {
		const target = link.replace(currentPath, targetPath);
		fs.readlink(link, (err, resolvedPath) => {
			if (err) {
				return console.log(err);
			}
			checkLink(resolvedPath, target);
		});
	};

	const checkLink = (resolvedPath, target) => {
		isWritable(target, (writable) => {
			if (writable) {
				return makeLink(resolvedPath, target);
			}
			fs.readlink(target, (err, targetDest) => {
				if (err) {
					return console.log(err);
				}

				if (targetDest === resolvedPath) {
					return cb();
				}
				return rmFile(target, () => {
					makeLink(resolvedPath, target);
				});
			});
		});
	};

	const makeLink = (linkPath, target) => {
		fs.symlink(linkPath, target, (err) => {
			if (err) {
				return console.log(err);
			}
			return cb();
		});
	};

	const isWritable = (path, done) => {
		fs.lstat(path, (err) => {
			if (err) {
				if (err.code === 'ENOENT') return done(true);
				return done(false);
			}
			return done(false);
		});
	};

	const cb = (skipped) => {
		if (!skipped) running--;
		finished++;
		if (started === finished && running === 0) {
			if (callback !== undefined) {
				return callback();
			}
		}
	};

	startCopy(currentPath);
};
